/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2023. All rights reserved.
 *
 */

import path from 'path';

import { FileSet, InputFile, TaskInputValue } from '@ohos/hvigor';
import fse from 'fs-extra';
import * as fs from 'fs-extra';

import { BuildArtifactConst, BuildDirConst } from '../../const/build-directory-const.js';
import { ArkTSVersionConst, CommonConst, CommonNodeConst, DefaultTargetConst } from '../../const/common-const.js';
import { AppJson } from '../../options/configure/app-json-options.js';
import { ModuleJson } from '../../options/configure/module-json-options.js';
import { CmakeUtil } from '../../utils/cmake-util.js';
import { minimizeHspDependencies } from '../../utils/dependency-util.js';
import { FileUtil } from '../../utils/file-util.js';
import { ohPackageLoader } from '../../utils/loader/file/oh-package-loader.js';
import { OhosLogger } from '../../utils/log/ohos-logger.js';
import {
  checkHasLocalFileDependency,
  collectLocalFileDependency,
  copyParameterizationLocalProductDependencies,
  getTypesFieldFromMainField
} from '../../utils/npm-utils.js';
import { TaskNames } from '../common/task-names.js';
import { HarExtendInfo } from '../har/har-extend-info.js';
import { TargetTaskService } from '../service/target-task-service.js';
import { OhosHapTask } from '../task/ohos-hap-task.js';

import AppOptObj = AppJson.AppOptObj;
import ModuleObj = ModuleJson.ModuleObj;
import ModuleOptObj = ModuleJson.ModuleOptObj;
import Task = TaskNames.Task;

import { getPropertyKeepDependency } from '../../utils/parameter-utils.js';
import { resolveJsonObjWithoutAtModule } from "../../validate/dependency-module-version-validate";

const _log = OhosLogger.getLogger('PrepareSharedHarRes');

/**
 * 生成打包shared library的package.json和module.json
 *
 * @since 2023/1/18
 */
export class PrepareSharedHarRes extends OhosHapTask {
  // inputs
  public readonly hasNativeOption: boolean;
  public readonly cmakeListDir: string | undefined;
  public readonly _harExtendInfo: HarExtendInfo;
  private readonly inputPackageJson5Path: string;
  private readonly inputModuleJsonPath: string;
  private readonly inputDeclareFilesPath: string;
  private readonly headerPath: string[];
  private readonly libsPath: string;
  private readonly inputResourceStr: string;
  private readonly inputPacJsonFilePath: string;

  // outputs
  private readonly taskTmpDir: string;
  private readonly outputPackageJsonPath: string;
  private readonly outputModuleJsonPath: string;
  private readonly outputDeclareFilesPath: string;
  private readonly outputResourceStr: string;
  private readonly outputPacJsonFilePath: string;

  private readonly INTERFACE_HAR: string = 'InterfaceHar';
  private readonly _moduleDir: string;

  // 增量判断需要忽略的文件,忽略的文件与static library一致
  private readonly ignoreFileList: string[] = [
    BuildDirConst.LIBS,
    BuildDirConst.BUILD_ROOT,
    CommonNodeConst.NODE_MODULES,
    CommonConst.OH_MODULES,
    '.cxx',
    '.previewer',
    '.hvigor',
    '.gitignore',
    '.ohpmignore',
  ];
  private readonly allInvalidPrefix: string[] = [];

  declareInputs(): Map<string, TaskInputValue> {
    const inputs = new Map<string, TaskInputValue>();
    const packageJsonObj = this.packageJsonObj;
    if (packageJsonObj) {
      inputs.set('modulePackageJsonObj', JSON.stringify(this.packageJsonObj));
    }
    inputs.set('integratedHsp', !!this.targetService.getBuildOption().arkOptions?.integratedHsp);
    inputs.set(CommonConst.KEEP_DEPENDENCY, !!getPropertyKeepDependency());
    return inputs;
  }

  declareInputFiles(): FileSet {
    const invalidPrefixes = this.allInvalidPrefix.join('|').replace(/\\/g, '\\\\');
    const regExp = RegExp(`^(?!${invalidPrefixes}).*`);
    const inputFiles = super
      .declareInputFiles()
      .addEntry(this.inputPackageJson5Path)
      .addEntry(this.inputModuleJsonPath)

      // 添加share模块对于目录的增量判断，regExp为增量判断忽略的文件
      .addEntry(this._moduleDir, { isDirectory: true, test: regExp })
      .addEntry(this.libsPath);
    if (fse.existsSync(this.inputDeclareFilesPath)) {
      inputFiles.addEntry(this.inputDeclareFilesPath, { isDirectory: true });
    }
    if (fse.existsSync(this.inputResourceStr)) {
      inputFiles.addEntry(this.inputResourceStr);
    }
    if (fse.existsSync(this.inputPacJsonFilePath)) {
      inputFiles.addEntry(this.inputPacJsonFilePath);
    }

    for (const pathItem of this.headerPath) {
      const headerDir = path.isAbsolute(pathItem) ? pathItem : path.resolve(this._moduleDir, pathItem);
      if (fs.pathExistsSync(headerDir)) {
        inputFiles.addEntry(headerDir);
      }
    }
    return inputFiles;
  }

  declareOutputFiles(): FileSet {
    return super.declareOutputFiles().addEntry(this.taskTmpDir, { isDirectory: true });
  }

  constructor(taskService: TargetTaskService) {
    super(taskService, Task.PREPARE_SHARED_HAR_RES);
    const buildOpt = this.targetService.getBuildOption();
    this.inputPackageJson5Path = taskService.getTargetData().getModuleModel().getOhPackageJson5Path();
    this.inputModuleJsonPath = this.pathInfo.getIntermediatesProcessProfile();
    this.inputResourceStr = this.pathInfo.getResourceStr();
    this.inputDeclareFilesPath = this.pathInfo.getIntermediatesEtsTgzPath(this.moduleName);
    this.inputPacJsonFilePath = this.pathInfo.getIntermediatesPacJsonPath();
    const headerPath = buildOpt.nativeLib?.headerPath;
    if (typeof headerPath === 'string') {
      this.headerPath = [headerPath];
    } else if (Array.isArray(headerPath)) {
      this.headerPath = headerPath;
    } else {
      this.headerPath = [];
    }
    this.libsPath = this.pathInfo.getIntermediatesStrippedLibsDir();

    this.taskTmpDir = this.getTaskTempDir(this.targetData, 'shared_har', false);
    this.outputPackageJsonPath = ohPackageLoader.getNodeOhPackagePath(path.resolve(this.taskTmpDir, CommonConst.OH_PACKAGE_JSON5));
    this.outputModuleJsonPath = path.resolve(this.taskTmpDir, 'src', 'main', CommonConst.MODULE_JSON);
    this.outputResourceStr = path.resolve(this.taskTmpDir, 'src', 'main', 'resource');
    this.outputDeclareFilesPath = this.taskTmpDir;
    this.outputPacJsonFilePath = path.resolve(this.taskTmpDir, CommonConst.PAC_JSON);
    this._moduleDir = this.moduleModel.getProjectDir();
    this.initAllInvalidPrefix();
    this._harExtendInfo = new HarExtendInfo(this.moduleModel, taskService.getBuildOption());
    const nativeFlag = CmakeUtil.nativeTaskCondition(this.moduleModel, this.targetData, buildOpt.externalNativeOptions);
    this.hasNativeOption = nativeFlag;
    this.cmakeListDir = nativeFlag ? CmakeUtil.getCmakeListDir(this.moduleModel, this.targetName, buildOpt.externalNativeOptions) : undefined;
  }

  initTaskDepends(): void {
    // 处理ohosTest差异化场景
    if (this.targetName === DefaultTargetConst.OHOS_TEST_TARGET) {
      // this.declareDepends('OhosTestCompileArkTS');
    } else {
      this.declareDepends('CompileArkTS');
    }
    this.declareDepends(TaskNames.Task.DO_NATIVE_STRIP.name);
  }

  protected async doTaskAction() {
    this.preCheckBeforePack();
    this.copyLocalDepsFileToTempDir();
    FileUtil.copySpecialFileToTempDir(this.moduleModel.getProjectDir(), this.taskTmpDir);
    if (fse.existsSync(this.inputDeclareFilesPath)) {
      fse.copySync(this.inputDeclareFilesPath, this.outputDeclareFilesPath);
    }

    if (this.targetService.getSelfArkTSVersion() === ArkTSVersionConst.ARKTS1_2) {
      const declgenV2OutPath = this.pathInfo.getIntermediatesDeclgenPath(this.packageJsonObj.name);
      if (fse.existsSync(declgenV2OutPath) && fse.statSync(declgenV2OutPath).isDirectory()) {
        fse.ensureDirSync(this.taskTmpDir);
        fse.copySync(declgenV2OutPath, this.taskTmpDir);
      }
    }

    this.copyIntermediatePacToTempDir();
    await this.generatePackageJson();
    this.generateModuleJson();
    this.copyResourcesTxt();
    this.initRes();
    await this.copyObfuscationRules();
    await this.syncNative();
  }

  private copyIntermediatePacToTempDir() {
    // 复制pac.json5文件到interfaceHar包
    if (fse.existsSync(this.inputPacJsonFilePath)) {
      fse.copySync(this.inputPacJsonFilePath, this.outputPacJsonFilePath);
    }
  }

  @InputFile() private get obfuscationRulesPath(): string {
    return this.targetService.getTargetData().getPathInfo().getObfuscationExportRulePath();
  }

  // 拷贝混淆规则文件到taskTmpDir
  private async copyObfuscationRules() {
    if (fse.existsSync(this.obfuscationRulesPath)) {
      await fse.copy(this.obfuscationRulesPath, path.join(this.taskTmpDir, CommonConst.OBFUSCATION_TXT));
    }
  }

  private copyResourcesTxt() {
    const resourcesPath = path.resolve(this.pathInfo.getIntermediatesRes(), BuildArtifactConst.RESOURCE_TABLE_TXT);
    if (fse.existsSync(resourcesPath)) {
      fse.copySync(resourcesPath, path.resolve(this.taskTmpDir, BuildArtifactConst.RESOURCE_TABLE_TXT));
    }
  }

  private async syncNative() {
    for (const pathItem of this.headerPath) {
      const fileMap = new Map<string, string>();
      const headerDir = path.isAbsolute(pathItem) ? pathItem : path.resolve(this._moduleDir, pathItem);
      CmakeUtil.checkNativeHeader(headerDir, this.moduleModel, fileMap);
      if (fse.existsSync(headerDir)) {
        await fse.copy(headerDir, path.resolve(this.taskTmpDir, 'include'), { recursive: true });
      }
    }

    // 当excludeSoFromInterfaceHar配置为true，则构建hsp时将不会把so打包到interface har中。仅作用于hsp
    if (fse.existsSync(this.libsPath) && fse.readdirSync(this.libsPath).length > 0 && !this.excludeSoFromInterfaceHar()) {
      await fse.copy(this.libsPath, path.resolve(this.taskTmpDir, 'libs'), { recursive: true });
    }
  }

  /**
   * 根据hsp模块的package.json，用main字段信息替换types字段，并删除main字段
   *
   * @private
   */
  private async generatePackageJson() {
    let packageJsonObj = this.packageJsonObj as any;
    packageJsonObj.types = getTypesFieldFromMainField(packageJsonObj.main);
    packageJsonObj.main = undefined;
    if (packageJsonObj.packageType && packageJsonObj.packageType !== this.INTERFACE_HAR) {
      _log.warn('share library packageType in oh-package.json should use InterfaceHar');
    }
    if (!packageJsonObj.packageType) {
      packageJsonObj.packageType = this.INTERFACE_HAR;
    }
    const useNormalizedOHMUrl = this.targetService.getBuildOption()?.strictMode?.useNormalizedOHMUrl;
    const relativePath = path.relative(this.pathInfo.getModuleBuildPath(), this.pathInfo.getGenerateBuildProfileDir());
    const sourceRoots = [
      ...(this.targetData.getTargetOpt().source?.sourceRoots || []),
      `./${BuildDirConst.SRC}/${BuildDirConst.MAIN}`,
      `./${this.pathInfo.getBuildRoot()}/${relativePath.replace(/\\/g, '/')}/${this.targetName}`,
    ];
    const integratedHsp = this.targetService.getBuildOption().arkOptions?.integratedHsp;
    const metadata = {
      useNormalizedOHMUrl,
      integratedHsp,
      sourceRoots,
      debug: this.targetService.isDebug(),
      arkTSVersion: this.targetService.getSelfArkTSVersion()
    };

    if (packageJsonObj.metadata) {
      Object.assign(packageJsonObj.metadata, metadata);
    } else {
      Object.assign(packageJsonObj, { metadata });
    }
    minimizeHspDependencies(packageJsonObj, this.service, this.packageManagerPath);

    let libsFileNameSet = new Set<string>();
    const libsDir = this.pathInfo.getIntermediatesProcessLibs();
    if (fs.existsSync(libsDir)) {
      libsFileNameSet = await FileUtil.readDirFileNames(libsDir);
    }

    const extraParams = CmakeUtil.getHarExtraParams(this);
    packageJsonObj = FileUtil.mergeExtraParamsToOhPack(packageJsonObj, extraParams, this.targetName, libsFileNameSet);
    resolveJsonObjWithoutAtModule(packageJsonObj);
    fse.outputJsonSync(this.outputPackageJsonPath, packageJsonObj, { encoding: 'utf8', spaces: '  ' });
  }

  private copyLocalDepsFileToTempDir() {
    collectLocalFileDependency(this.inputPackageJson5Path, this.packageJsonObj).forEach((localDepPath) => {
      const absolutePath = path.resolve(this._moduleDir, localDepPath);
      if (fse.pathExistsSync(absolutePath)) {
        // 重新计算开发者配置的相对路径, 保证相对路径是相对于模块根路径的相对路径
        fs.copySync(absolutePath, path.resolve(this.taskTmpDir, path.relative(this._moduleDir, absolutePath)));
      }
    });

    // 只有在 oh-package.json5 中以 @param: 参数化依赖了本地产物(如file:../lib.har)时 localProductDependenciesPath 才不会为空，此时本地产物依赖需要打进 har 包
    if (this.localProductDependenciesPath.length > 0) {
      copyParameterizationLocalProductDependencies({
        localProductDependencies: this.localProductDependenciesPath,
        moduleDir: this._moduleDir,
        tempDir: this.taskTmpDir,
        logger: _log,
      });
    }
  }

  /**
   * 根据hsp模块的module.json，筛选指定字段作为har包的module.json的字段
   *
   * @private
   */
  private generateModuleJson() {
    const moduleJsonObj: ModuleOptObj & AppOptObj = fse.readJsonSync(this.inputModuleJsonPath, { encoding: 'utf8' });
    let bundleName = moduleJsonObj.app.bundleName;
    const integratedHsp = this.targetService.getBuildOption().arkOptions?.integratedHsp;

    // 如果当前模块时hsp且配置了integratedHsp为true则将pack.info的bundleName置空
    if (this.moduleModel.isHspModule() && integratedHsp) {
      bundleName = '';
    }
    moduleJsonObj.app = {
      bundleName,
      bundleType: moduleJsonObj.app.bundleType,
      debug: moduleJsonObj.app.debug,
      versionCode: moduleJsonObj.app.versionCode,
      versionName: moduleJsonObj.app.versionName,
      minCompatibleVersionCode: moduleJsonObj.app.minCompatibleVersionCode,
      minAPIVersion: moduleJsonObj.app.minAPIVersion,
      targetAPIVersion: moduleJsonObj.app.targetAPIVersion,
      apiReleaseType: moduleJsonObj.app.apiReleaseType,
      targetMinorAPIVersion: moduleJsonObj.app.targetMinorAPIVersion,
      targetPatchAPIVersion: moduleJsonObj.app.targetPatchAPIVersion,
    };
    moduleJsonObj.module = {
      name: moduleJsonObj.module.name,
      type: moduleJsonObj.module.type,
      deliveryWithInstall: moduleJsonObj.module.deliveryWithInstall,
      deviceTypes: moduleJsonObj.module.deviceTypes,
      requestPermissions: moduleJsonObj.module.requestPermissions,
      dependencies: moduleJsonObj.module.dependencies,
      libIsolation: moduleJsonObj.module.libIsolation,
    } as ModuleObj;

    fse.outputJsonSync(this.outputModuleJsonPath, moduleJsonObj, { encoding: 'utf8' });
  }

  private preCheckBeforePack() {
    const packagePath = this.moduleModel.getOhPackageJson5Path();
    const packageJsonObj = this.packageJsonObj;

    // 检查package.json依赖中有没有依赖本地文件或文件夹
    if (checkHasLocalFileDependency(packagePath, packageJsonObj)) {
      _log
        ._buildError(
          `Local dependencies detected during har packing of module ${this.moduleName}. ` +
            'Declaring local dependencies in har module might cause failing during install har package.',
        )
        ._detail('Check oh-package.json5 file of current shared library module and replace the local dependencies.')
        ._file(packagePath)
        ._printWarn(this.moduleName);
    }

    // 初始化最后用于打包的temp文件夹
    fse.emptyDirSync(this.taskTmpDir);
    return true;
  }

  // 初始化增量需要忽略的文件路径
  private initAllInvalidPrefix() {
    for (const ignoreFile of this.ignoreFileList) {
      this.allInvalidPrefix.push(path.resolve(this._moduleDir, ignoreFile));
    }
  }

  private initRes() {
    if (fse.existsSync(this.pathInfo.getResourceStr())) {
      fse.copySync(this.inputResourceStr, this.outputResourceStr);
    }
  }

  // 通过excludeSoFromInterfaceHar和headerPath的内容判断是否将so打入到hsp的 interface har中
  private excludeSoFromInterfaceHar(): boolean {
    const buildOpt = this.targetService.getBuildOption();
    const headerPath = buildOpt.nativeLib?.headerPath;
    const excludeSoFromInterfaceHar = buildOpt.nativeLib?.excludeSoFromInterfaceHar ?? false;
    if (headerPath && excludeSoFromInterfaceHar) {
      _log.warn(`When headerPath is set, .so and header files must be included in the interface HAR for C++ API exposure and .so linking. 
              excludeSoFromInterfaceHar: true has no effect in this case.
              Check the headerPath and excludeSoFromInterfaceHar configuration in ${path.resolve(this.pathInfo.getModulePath(), 'build-profile.json5')}.`);
      return false;
    }
    return excludeSoFromInterfaceHar;
  }
}
